/**
 * This file is part of 1genia trampoline
 * Copyright (C) 2007 1genia (contact@1genia.com)
 *
 * This library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; version 3 of the License. 
 *
 * This library is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details. 
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; see the file COPYING.TXT.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street,
 * Fifth Floor, Boston, MA 02110-1301, USA. 
 **/
package com.genia.toolbox.uml_generator.manager.impl;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.genia.toolbox.basics.bean.impl.ConjunctionPredicate;
import com.genia.toolbox.basics.exception.BundledException;
import com.genia.toolbox.basics.exception.technical.TechnicalIOException;
import com.genia.toolbox.uml_generator.Constants;
import com.genia.toolbox.uml_generator.bean.ModelGeneratorContext;
import com.genia.toolbox.uml_generator.manager.ModelGenerator;
import com.genia.toolbox.uml_generator.model.AbstractClassModel;
import com.genia.toolbox.uml_generator.model.AssociationCardinality;
import com.genia.toolbox.uml_generator.model.AssociationModel;
import com.genia.toolbox.uml_generator.model.AssociationType;
import com.genia.toolbox.uml_generator.model.AttributeModel;
import com.genia.toolbox.uml_generator.model.DataTypeModel;
import com.genia.toolbox.uml_generator.model.impl.AttributeModelImpl;

/**
 * print the dependency classes.
 */
public class DependencyGeneratorImpl
    extends AbstractManager
    implements ModelGenerator
{

  /**
   * this class keep all the call to other method to do for each kind of
   * dependency.
   */
  private class DependencyCalls
  {
    /**
     * the attributes needed by the current dependency.
     */
    private List<AttributeModel> attributes = new ArrayList<AttributeModel>();

    /**
     * the methods to call in the onCreate method.
     */
    private List<String> onCreateCalls = new ArrayList<String>();

    /**
     * the methods to call in the onDelete method.
     */
    private List<String> onDeleteCalls = new ArrayList<String>();

    /**
     * the methods to call in the onPostCreate method.
     */
    private List<String> onPostCreateCalls = new ArrayList<String>();

    /**
     * the methods to call in the onUpdate method.
     */
    private List<String> onUpdateCalls = new ArrayList<String>();



    /**
     * returns whether all the calls are empty or not.
     * 
     * @return whether all the calls are empty or not
     */
    public boolean isEmpty()
    {
      return onCreateCalls.isEmpty() && onPostCreateCalls.isEmpty() && onUpdateCalls.isEmpty() && onDeleteCalls.isEmpty();
    }
  }

  /**
   * {@link ThreadLocal} containing the association pointing to a class.
   */
  private final ThreadLocal<Map<AbstractClassModel, Set<AssociationModel>>> otherSideUniAssociations = new ThreadLocal<Map<AbstractClassModel, Set<AssociationModel>>>();



  /**
   * the generator.
   * 
   * @param context
   *          the context of the generation
   * @throws BundledException
   *           when an error occurred
   * @see com.genia.toolbox.uml_generator.manager.ModelGenerator#generate(com.genia.toolbox.uml_generator.bean.ModelGeneratorContext)
   */
  public void generate(ModelGeneratorContext context)
      throws BundledException
  {
    otherSideUniAssociations.set(getOtherSideUniAssociations(context));
    try {
      for (AbstractClassModel classModel : getCollectionManager().filterSet(context.getClasses().values(), Constants.KEEP_ONLY_INTERFACE_PREDICATE)) {
        generateDependency(context, classModel);
      }
    }
    finally {
      otherSideUniAssociations.remove();
    }
  }



  /**
   * generate the dependencies for cleaning the associations.
   * 
   * @param dependencyCalls
   *          the context to store the code
   * @param context
   *          the context of the generation
   * @param classModel
   *          the class to write the dependencies for
   * @throws TechnicalIOException
   *           if an error occured
   */
  private void generateCleaningDependency(DependencyCalls dependencyCalls, ModelGeneratorContext context, AbstractClassModel classModel)
      throws TechnicalIOException
  {
    Set<AssociationModel> uniAssociations = otherSideUniAssociations.get().get(classModel);
    if (uniAssociations == null) {
      uniAssociations = new HashSet<AssociationModel>();
    }
    Set<AssociationModel> currentSideAssocations = new HashSet<AssociationModel>();
    for (AssociationModel association : classModel.getAssociations()) {
      if (isRootAssociation(classModel, association) && !(classModel.hasStereotype(Constants.IGNORE_STEREOTYPE) && association.getAssociatedType().hasStereotype(Constants.IGNORE_STEREOTYPE))) {
        currentSideAssocations.add(association);
      }
    }
    if (uniAssociations.isEmpty() && currentSideAssocations.isEmpty()) {
      return;
    }

    StringBuilder out = new StringBuilder();
    out.append("{");
    final DataTypeModel persistablesSet = getModelDecorator().getGenericObject(Constants.SET_MODEL, Constants.OBJECT_MODEL);
    out.append("final ").append(persistablesSet.getQualifiedName()).append(" objectsToDelete = ").append(persistablesSet.getDefaultValue()).append(";\n");

    for (AssociationModel association : uniAssociations) {
      DataTypeModel criteria = getModelDecorator().getGenericObject(Constants.CRITERIA_MODEL, association.getCurrentType());
      out.append("{\n");
      out.append("final ").append(criteria.getQualifiedName()).append(" criteria = getCriteria(").append(association.getCurrentType().getQualifiedName()).append(".class);\n");
      out.append("criteria.addRestriction(getRestrictionsManager().");
      if (association.getOtherCardinality() == AssociationCardinality.ONE) {
        out.append("equals");
      }
      else {
        out.append("isIn");
      }
      out.append("(getConstantField(object), ");
      if (association.getOtherCardinality() == AssociationCardinality.MANY) {
        out.append("elements(");
      }
      out.append("getPropertyField(").append(association.getCurrentType().getQualifiedName()).append(".NAME_").append(getNameManager().getUpperUnderlineName(association)).append(")");
      if (association.getOtherCardinality() == AssociationCardinality.MANY) {
        out.append(")");
      }
      out.append("));\n");
      if (association.getOtherAssociationType() == AssociationType.COMPOSITE) {
        out.append("objectsToDelete.addAll(simpleFind(criteria));\n");
      }
      out.append("for (").append(association.getCurrentType().getQualifiedName()).append(" otherObject : simpleFind(criteria)) {\n");
      if (association.getOtherCardinality() == AssociationCardinality.ONE) {
        out.append("otherObject.set").append(getNameManager().getUpperCaseFirstLetterName(association)).append("(null);\n");
      }
      else {
        out.append("otherObject.remove").append(getNameManager().getUpperCaseFirstLetterName(association)).append("(object);\n");
      }
      out.append("}\n");
      out.append("}\n");
    }

    if (!currentSideAssocations.isEmpty()) {
      for (AssociationModel association : currentSideAssocations) {
        if (association.getOtherCardinality() == AssociationCardinality.ONE) {
          out.append("if(object.get").append(getNameManager().getUpperCaseFirstLetterName(association)).append("() != null) {\n");
          if (association.getAssociationType() == AssociationType.COMPOSITE) {
            out.append("objectsToDelete.add(").append("object.get").append(getNameManager().getUpperCaseFirstLetterName(association)).append("());");
          }
          out.append("object.set").append(getNameManager().getUpperCaseFirstLetterName(association)).append("(null);\n");
          out.append("}\n");
        }
        else {
          out.append("{\n");
          DataTypeModel setModel = getModelDecorator().getGenericObject(Constants.SET_MODEL, association.getAssociatedType());
          out.append(setModel.getQualifiedName()).append(" otherSet = ").append(setModel.getDefaultValue()).append(";");
          out.append("otherSet.addAll(").append("object.get").append(getNameManager().getUpperCaseFirstLetterName(association)).append("s()");
          if (association.getQualifier() != null) {
            out.append(".values()");
          }
          out.append(");\n");
          if (association.getAssociationType() == AssociationType.COMPOSITE) {
            out.append("objectsToDelete.addAll(otherSet);\n");
          }
          out.append("for(").append(association.getAssociatedType().getQualifiedName()).append(" other : otherSet) {\n");
          out.append("object.remove").append(getNameManager().getUpperCaseFirstLetterName(association)).append("(other);\n");
          out.append("}\n");
          out.append("}\n");
        }
      }
      out.append("update((").append(Constants.PERSISTABLE_MODEL.getQualifiedName()).append("<? extends java.io.Serializable>)object);");
    }

    out.append("for(").append(Constants.OBJECT_MODEL.getQualifiedName()).append(" objetToDelete : objectsToDelete) { delete((").append(Constants.PERSISTABLE_MODEL.getQualifiedName()).append(
        ")objetToDelete);}\n");
    out.append("}");
    dependencyCalls.onDeleteCalls.add(out.toString());
  }



  /**
   * generate the dependency class for an interace.
   * 
   * @param context
   *          the context of the generation
   * @param classModel
   *          the class to write the dependencies for
   * @throws BundledException
   *           if an error occured
   */
  private void generateDependency(ModelGeneratorContext context, AbstractClassModel classModel)
      throws BundledException
  {
    DependencyCalls dependencyCalls = new DependencyCalls();

    generateEscapeDependency(dependencyCalls, classModel);
    generateCleaningDependency(dependencyCalls, context, classModel);

    if (dependencyCalls.isEmpty()) {
      return;
    }

    AbstractClassModel dependencyObject = getModelDecorator().getDependencyObject(classModel);
    context.getDependencies().add(dependencyObject);
    File classFile = new File(new File(context.getDirectoryForPackage(context.getConfiguration().getGeneratedSrcDir(), context.getConfiguration().getConfigurationPackage()),
        Constants.DEPENDENCY_PACKAGE_SUFFIX), dependencyObject.getName() + ".java");
    classFile.getParentFile().mkdirs();
    try {
      PrintWriter out = new PrintWriter(new FileOutputStream(classFile));
      // context.getGeneratedSources().add(classFile);

      out.append("package ").append(context.getConfiguration().getConfigurationPackage()).append(".").append(Constants.DEPENDENCY_PACKAGE_SUFFIX).append(";\n");

      getPrinterManager().printComment(out, "dependency class to handle " + classModel.getQualifiedName() + ".", Constants.COMMENT_WARNING_GENERATED);
      out.append("public class ").append(dependencyObject.getName()).append("\n");
      out.append("extends ").append(getModelDecorator().getGenericObject(Constants.PERSISTABLE_DEPENDENCY_MODEL, classModel).getQualifiedName());
      out.append("{\n");

      getPrinterManager().printComment(out, "constructor.");
      out.append("public ").append(dependencyObject.getName()).append("() {\n");
      out.append("super(").append(classModel.getQualifiedName()).append(".class);\n");
      out.append("}\n");

      for (AttributeModel attributeModel : dependencyCalls.attributes) {
        getPrinterManager().printAttribute(out, attributeModel);
        getPrinterManager().printMethod(out, getAttributeManager().getGetter(attributeModel));
        getPrinterManager().printMethod(out, getAttributeManager().getSetter(attributeModel));
      }

      printMethod(out, classModel, "onCreate", dependencyCalls.onCreateCalls);
      printMethod(out, classModel, "onPostCreate", dependencyCalls.onPostCreateCalls);
      printMethod(out, classModel, "onUpdate", dependencyCalls.onUpdateCalls);
      printMethod(out, classModel, "onDelete", dependencyCalls.onDeleteCalls);

      out.append("}\n");
      out.close();
    }
    catch (IOException e) {
      throw getExceptionManager().convertException(e);
    }

  }



  /**
   * generate the dependency for field escape.
   * 
   * @param dependencyCalls
   *          the context to store the code
   * @param classModel
   *          the class to write the dependencies for
   * @throws BundledException
   *           if an error occured
   */
  private void generateEscapeDependency(DependencyCalls dependencyCalls, AbstractClassModel classModel)
      throws BundledException
  {
    // ignored class must not be considered.
    if (classModel.hasStereotype(Constants.IGNORE_STEREOTYPE)) {
      return;
    }
    @SuppressWarnings("unchecked")
    Set<AttributeModel> toEscapeAttributes = getCollectionManager().filterSet(classModel.getAttributes(),
        new ConjunctionPredicate<AttributeModel>(Constants.KEEP_TO_ESCAPE_PREDICATE, Constants.REMOVE_UN_MAPPABLE_PREDICATE));
    if (toEscapeAttributes.isEmpty()) {
      return;
    }
    AttributeModel stringManagerAttribute = new AttributeModelImpl();
    stringManagerAttribute.setComment("the {@link " + Constants.STRING_MANAGER_MODEL.getQualifiedName() + "} to use.");
    stringManagerAttribute.setName("stringManager");
    stringManagerAttribute.setType(Constants.STRING_MANAGER_MODEL);
    stringManagerAttribute.setInitialValue("null");
    dependencyCalls.attributes.add(stringManagerAttribute);
    StringBuilder out = new StringBuilder();
    for (AttributeModel attributeModel : toEscapeAttributes) {
      out.append("object.setEscaped").append(getNameManager().getUpperCaseFirstLetterName(attributeModel)).append("(getStringManager().removeAccents(object.").append(
          getAttributeManager().getGetter(attributeModel).getName()).append("()));");
    }
    dependencyCalls.onCreateCalls.add(out.toString());
    dependencyCalls.onUpdateCalls.add(out.toString());
  }



  /**
   * returns the unidirectional association index by their destination class.
   * 
   * @param context
   *          the context of the generation
   * @return the unidirectional association index by their destination class
   * @throws BundledException
   *           when an error occurred
   */
  private Map<AbstractClassModel, Set<AssociationModel>> getOtherSideUniAssociations(ModelGeneratorContext context)
      throws BundledException
  {
    Map<AbstractClassModel, Set<AssociationModel>> res = new HashMap<AbstractClassModel, Set<AssociationModel>>();
    for (AbstractClassModel classModel : getCollectionManager().filterSet(context.getClasses().values(), Constants.KEEP_ONLY_INTERFACE_PREDICATE)) {
      for (AssociationModel association : classModel.getAssociations()) {
        if (association.getOtherEndAssociation() == null && isRootAssociation(classModel, association)
            && !(classModel.hasStereotype(Constants.IGNORE_STEREOTYPE) && association.getAssociatedType().hasStereotype(Constants.IGNORE_STEREOTYPE))) {
          Set<AssociationModel> associations = res.get(association.getAssociatedType());
          if (associations == null) {
            associations = new HashSet<AssociationModel>();
            res.put(association.getAssociatedType(), associations);
          }
          associations.add(association);
        }
      }
    }
    return res;
  }



  /**
   * returns wether an association is declared for the first time in the given
   * interface.
   * 
   * @param classModel
   *          the interface to inspect
   * @param association
   *          the association to inspect
   * @return <code>true</code> if the association is not inherited
   */
  private boolean isRootAssociation(AbstractClassModel classModel, AssociationModel association)
  {
    if (!classModel.getAssociations().contains(association)) {
      return false;
    }
    for (AbstractClassModel interfaceModel : classModel.getImplementsModel()) {
      if (isRootAssociation(interfaceModel, association)) {
        return false;
      }
    }
    return true;
  }



  /**
   * print the method given in argument with the actions given in argument.
   * 
   * @param out
   *          the {@link PrintWriter} to print to
   * @param classModel
   *          the class to write the dependencies for.
   * @param methodName
   *          the name of the method to print
   * @param methodActions
   *          the actions to put in the method body
   * @throws TechnicalIOException
   *           if an error occured
   */
  private void printMethod(PrintWriter out, AbstractClassModel classModel, String methodName, List<String> methodActions)
      throws TechnicalIOException
  {
    if (methodActions.isEmpty()) {
      return;
    }
    getPrinterManager().printComment(out, "handle the dependencies.", "@param object the object that trigger the dependency",
        "@throws " + Constants.TECHNICAL_EXCEPTION_MODEL.getQualifiedName() + " when an error occurs");
    out.append("@Override\n");
    out.append("@SuppressWarnings(\"unchecked\")\n");
    out.append("public void ").append(methodName).append("(final ").append(classModel.getQualifiedName()).append(" object) throws ").append(Constants.TECHNICAL_EXCEPTION_MODEL.getQualifiedName())
        .append(" {\n");
    for (String methodAction : methodActions) {
      out.append(methodAction);
    }
    out.append("}\n");

  }
}
